/*******************************************************************************
 * @file Copyright 2009 The CREATE Lab at CMU
 * 
 *       This file is part of JubJub, an open source project.
 *       http://code.google.com/p/jubjub/
 * 
 *       Redistribution and use in source and binary forms, with or without
 *       modification, are permitted provided that the following conditions are
 *       met:
 * 
 *       Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 * 
 *       Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 * 
 *       Neither the name of Carnegie Mellon nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 * 
 *       THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *       "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *       LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *       PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *       HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *       SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *       LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *       DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *       THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *       OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package edu.cmu.ri.createlab.jj.blocks;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import edu.cmu.ri.createlab.jj.blocks.BlockDefs.AccessTypes;
import edu.cmu.ri.createlab.jj.blocks.BlockDefs.BlockTypes;
import edu.cmu.ri.createlab.jj.blocks.BlockDefs.VarTypes;
import edu.cmu.ri.createlab.jj.visitors.*;

/**
 * 
 * @author Erik Pasternak
 *
 * RefBlock allows a single Block to be used in many places.
 * 
 * RefBlock is a way to create calls to a single block of code and mimics
 * method calls or references to a variable in Java/C. The idea being that
 * a block should only be defined once and then used in many places. Any
 * changes to the underlying block will then affect all references to that
 * block. Each reference also maintains the parameters and returns that are
 * being used with that instance of the call.
 */
public class RefBlock
{
   ///Name of the reference
   private String name;
   ///A unique ID for this reference (unique among blocks and refs)
   private int id;
   ///The index to use when referencing an array or FlowBlock. This only supports 1d arrays.
   private int index;
   ///The comment for this block
   private String comment;
   ///Whether this reference is valid and complete, true means there is
   ///an error, false means no problems.
   private boolean error;
   /**
    * All the parameters that are being passed in for this call.
    * 
    * When this block is referencing an ObjectBlock the first parameter should
    * be the RefBlock for the constructor, variable, or method of the object
    * that is being called. When using an object as itself the VarBlock
    * representation of the Object should be used.
    */
   private List<RefBlock> params;
   ///What vars are being assigned by this call
   private List<RefBlock> returns;
   
   ///This is the block it references
   private BlockInterface block;
   ///This is the block the reference appears in, null if in the palate
   private BlockInterface owner;

   /**
    * 
    * @param id Unique ID for this reference
    * @param block the block that this references
    * 
    * Creates a new RefBlock with a link to the Block it references and
    * unique id. If the block is not null the name is set to the name
    * for the block referenced.
    */
   public RefBlock(int id, BlockInterface block)
   {
      ///There's no reason the block should be null but just in case
      if(block != null) 
      {
         this.name = block.getName();
         block.addRef(this);
      }
      else this.name = "";
      ///a unique id
      this.id = id;
      this.block = block;
      ///params, returns, and owner always start out null
      this.params = new LinkedList<RefBlock>();
      this.returns = new LinkedList<RefBlock>();
      this.owner = null;
      this.comment = "";
      this.setIndex(-1);
      this.update();
   }
   
   public RefBlock clone(int id) throws Exception
   {
      BlockInterface tempBlock;
      if(block.getType() == BlockTypes.VAR && ((VarBlock)block).isLiteral()) 
         tempBlock = ((VarBlock)block).clone(0);
      else if(block.getType() == BlockTypes.COMMENT) tempBlock = ((CommentBlock)block).clone(0);
      else tempBlock = block;
      RefBlock rb = new RefBlock(id, tempBlock);
      Iterator<RefBlock> it = this.params.iterator();
      while(it.hasNext()) rb.addParam(it.next().clone(id));
      it = this.returns.iterator();
      while(it.hasNext()) rb.addReturn(it.next().clone(id));
      rb.setIndex(this.index);
      rb.setComment(this.comment);
      rb.setName(this.name);
      return rb;
   }

   ///Sets the comment for this block
   public void setComment(String comment)
   {
      this.comment = comment;
   }
   ///Gets the comment for this block
   public String getComment()
   {
      return comment;
   }
   
   ///Sets the index for accessing single elements if an array is referenced.
   ///Should be set to -1 when referencing the entire object or a non-array.
   public void setIndex(int index)
   {
      this.index = index;
   }

   ///Returns the array index currently set for this reference.
   public int getIndex()
   {
      return index;
   }

   /**This allows a block visitor to visit it. The BV will have to decide
    * if it needs to visit the block this references as well.
    */
   public BVReturn accept(BlockVisitorInterface bv)
   {
      BVReturn result = bv.visit(this);
      return result;
   }
   
   ///Updates this reference when the block it references changes.
   ///Currently, just checks to ensure it's still valid and if it's full.
   ///If not full and valid it should let its owner know there's an error.
   public boolean update()
   {
      ///Update all the owner info for the refs this ref owns
      if(params != null)
      {
         Iterator<RefBlock> it = params.iterator();
         while(it.hasNext())
         {
            it.next().setOwner(this.owner);
         }
      }
      if(returns != null)
      {
         Iterator<RefBlock> it = returns.iterator();
         while(it.hasNext())
         {
            it.next().setOwner(this.owner);
         }
      }
      if(isValid())
      {
         //name = block.getName();
      }
      if(!isValid() || !isFilled())
      {
         this.error = true;
         if(this.owner != null) this.owner.checkError();
      }
      else if(this.error)
      {
         this.error = false;
         if(this.owner != null) this.owner.checkError();
      }
      return this.error;
   }
   
   ///Checks to see if there is currently an error with this ref
   public void checkError()
   {
      if(!isValid() || !isFilled())
      {
         this.error = true;
         if(this.owner != null) this.owner.checkError();
      }
      else if(this.error)
      {
         this.error = false;
         if(this.owner != null) this.owner.checkError();
      }
   }
   
   ///Returns the current error state
   public boolean hasError()
   {
      return this.error;
   }

   ///Returns the unique ID for this reference.
   public int getID()
   {
      return id;
   }

   ///Returns the name of this reference
   public String getName()
   {
      return name;
   }
   
   ///Set the name of this reference
   public void setName(String name)
   {
      this.name = name;
   }
   
   ///Returns whether or not this reference still refers to a valid block.
   public boolean isValid()
   {
      if(block == null) return false;
      if(block.isValid())
      {
         if(block.getType() != BlockTypes.OBJECT) return true;
         if(params.size() < 1) return false;
         return params.get(0).isValid();
      }
      return false;
   }
   
   ///Sets the block this is referencing, will also remove itself from
   ///any block it was previously referencing and add itself as a reference
   ///to the new block.
   public void setBlock(BlockInterface block)
   {
      if(this.block != null)
      {
         this.block.remRef(this);
      }
      this.block = block;
      if(this.block != null) this.block.addRef(this);
      update();
   }
   
   ///Sets the block this is referencing to null. This is for use by
   ///the destroyBlock method of blocks so that it can iterate without
   ///the ref removing itself.
   public void clearBlock()
   {
      this.block = null;
      update();
   }
   
   ///Sets the owner of this reference(what block it appears in)
   ///and also sets the owner for any references it has.
   public void setOwner(BlockInterface owner)
   {
      this.owner = owner;
      this.update();
   }
   
 ///returns the Block this is referencing
   public BlockInterface getBlock()
   {
      return block;
   }
   
   ///returns the Block this appears in
   public BlockInterface getOwner()
   {
      return owner;
   }
   
   ///adds a parameter into the params list
   public void addParam(RefBlock param)
   {
      this.params.add(param);
   }
   
   ///inserts a parameter into the params list
   public void addParam(RefBlock param, int index)
   {
      this.params.add(index, param);
   }
   
   ///removes the specified parameter from params
   public void remParam(RefBlock param)
   {
      this.params.remove(param);
   }
   
   ///adds a return into the returns list
   public void addReturn(RefBlock ret)
   {
      this.returns.add(ret);
   }
   
   ///inserts a return into the returns list
   public void addReturn(RefBlock ret, int index)
   {
      this.returns.add(index, ret);
   }
   
   ///removes the specified return from returns
   public void remReturn(RefBlock ret)
   {
      this.returns.remove(ret);
   }
   
   ///gets the parameters used for this instance
   public List<RefBlock> getParams()
   {
      return this.params;
   }
   
   ///gets the returns used for this instance
   public List<RefBlock> getReturns()
   {
      return this.returns;
   }
   
   private String getVarSwapName(RefBlock ref)
   {
      String s = "";
      VarBlock tempBlock = (VarBlock)ref.getBlock();
      if(tempBlock.isLiteral())
         s = tempBlock.getValue(0);
      else
         s = tempBlock.getCallName();
      return s;
   }
   
   private String getExpSwapName(RefBlock ref)
   {
      String s = "";
      String first = "";
      String second = "";
      String symbol = "";
      if(ref.getParams().get(0).getBlock().getType() == BlockTypes.VAR)
         first = getVarSwapName(ref.getParams().get(0));
      else if(ref.getParams().get(0).getBlock().getType() == BlockTypes.EXPRESSION)
         first = getExpSwapName(ref.getParams().get(0));
      
      if(ref.getParams().get(1).getBlock().getType() == BlockTypes.VAR)
         second = getVarSwapName(ref.getParams().get(1));
      else if(ref.getParams().get(1).getBlock().getType() == BlockTypes.EXPRESSION)
         second = getExpSwapName(ref.getParams().get(1));
      
      symbol = ((ExpressionBlock)ref.getBlock()).getSymbol();
      
      s = first+symbol+second;
      
      return s;
   }
   
   ///returns a String array containing all the swapnames to be used
   ///in this call
   public String[] getSwapNames()
   {
      String[] swapnames;
      int namesLength = 0;
      if(params != null) namesLength += params.size();
      if(returns != null) namesLength += returns.size();
      if(namesLength > 0) 
      {
         swapnames = new String[namesLength];
         for(int i = 0; i < namesLength; i++)
         {
            ///fill in the swapnames first from the params list then the returns list
            if(i < params.size())
            {
               if(params.get(i).getBlock().getType() == BlockTypes.VAR)
               {
                  swapnames[i] = getVarSwapName(params.get(i));
               }
               else if(params.get(i).getBlock().getType() == BlockTypes.EXPRESSION)
               {
                  swapnames[i] = getExpSwapName(params.get(i));
               }
               else swapnames[i] = Block.toCallName(params.get(i).getName());
            }
            else
               swapnames[i] = Block.toCallName(returns.get(i-params.size()).getName());
         }
      }
      else swapnames = null;
      
      return swapnames;
   }
   
   ///Removes this reference from the list of references for its block and
   ///then clears itself. This is to ensure no pointers are associated with
   ///this prior to 'deleting' this reference. As nice as the garbage collector
   ///is I wish you could explicitly delete something.
   public void remove()
   {
      if(this.block != null)
      {
         this.block.remRef(this);
      }
      this.owner = null;
      this.block = null;
      this.name = null;
      Iterator<RefBlock> it;
      it = this.params.iterator();
      while(it.hasNext())
      {
         ///grab the reference locally
         RefBlock tempRB = it.next();
         ///remove it from the list to ensure you don't hit a recursive loop
         it.remove();
         ///perform its removal operation.
         tempRB.remove();
      }
      this.params.clear();
      it = this.returns.iterator();
      while(it.hasNext())
      {
         ///grab the reference locally
         RefBlock tempRB = it.next();
         ///remove it from the list to ensure you don't hit a recursive loop
         it.remove();
         ///perform its removal operation.
         tempRB.remove();
      }
      this.returns.clear();
   }
   

   /**
    * This checks its params and returns against the block it references
    * and returns whether it has the correct number of parameters and returns.
    * 
    * This checks the information it has against the block that it references.
    * It will scan through all the params and returns and make sure they're all
    * set correctly and match up with the block underneath. 
    */
   public boolean isFilled()
   {
      switch(block.getType())
      {
         case BLOCK:
         {
            BlockBlock tempBlock = (BlockBlock)block;
            if(params == null && tempBlock.getParams() != null) return false;
            if(params != null)
            {
               if(tempBlock.getParams() == null) return false;
               if(params.size() != tempBlock.getParams().size()) return false;
            }
            if(returns == null && tempBlock.getReturns() != null) return false;
            if(returns != null)
            {
               if(tempBlock.getReturns() == null) return false;
               if(returns.size() != tempBlock.getReturns().size()) return false;
            }
            break;
         }
         case CODE:
         {
            CodeBlock tempBlock = (CodeBlock)block;
            if(params == null && tempBlock.getParams() != null) return false;
            if(params != null)
            {
               if(tempBlock.getParams() == null) return false;
               if(params.size() != tempBlock.getParams().size()) return false;
            }
            if(returns == null && tempBlock.getReturns() != null) return false;
            if(returns != null)
            {
               if(tempBlock.getReturns() == null) return false;
               if(returns.size() != tempBlock.getReturns().size()) return false;
            }
            break;
         }
         case FLOW:
         {
            break;
         }
         //TODO Add code for checking Flow and Object blocks
         ///If it shouldn't have params/returns and it does it's an error.
         default:
            if(returns != null || params != null) return false;
      }
      return true;
   }
   
   ///Returns a readable string representation of this reference block.
   public String toString()
   {
      String s;
      s = "Type: RefBlock";
      s += " Name:" + this.name;
      s += " ID:" + this.id;
      if(params != null)
      {
         s += " Params:";
         for(int i = 0; i < params.size(); i++)
         {
            s += params.get(i).getName() + ", ";
         }
      }
      if(returns != null)
      {
         s += " Returns:";
         for(int i = 0; i < returns.size(); i++)
         {
            s += returns.get(i).getName() + ", ";
         }
      }
      s += " Block: ";
      if(block != null) s += block.getName();
      s += " Owner: ";
      if(owner != null) s += owner.getName();
      s += " Full:" + this.isFilled();
      return s;
   }
   
   ///Test method for RefBlock
   public static void main(String[] args)
   {
    ///Create an inline bit of code
      CodeBlock cb1, cb2, cb3, cb4;
      LinkedList<TaggedCodePiece> tc1 = new LinkedList<TaggedCodePiece>();
      tc1.add(new TaggedCodePiece("x",0));
      tc1.add(new TaggedCodePiece(" = "));
      tc1.add(new TaggedCodePiece("point",2));
      tc1.add(new TaggedCodePiece(".getX();\n"));
      tc1.add(new TaggedCodePiece("y",1));
      tc1.add(new TaggedCodePiece(" = "));
      tc1.add(new TaggedCodePiece("point",2));
      tc1.add(new TaggedCodePiece(".getY();\n"));
      ///Create a piece of code that can be called
      LinkedList<TaggedCodePiece> tc2 = new LinkedList<TaggedCodePiece>();
      tc2.add(new TaggedCodePiece("System.out.println("));
      tc2.add(new TaggedCodePiece("s",0));
      tc2.add(new TaggedCodePiece(");\n"));
      LinkedList<VarBlock> p1 = new LinkedList<VarBlock>();
      LinkedList<VarBlock> r1 = new LinkedList<VarBlock>();
      LinkedList<VarBlock> p2 = new LinkedList<VarBlock>();
      LinkedList<VarBlock> r2 = null;
      try
      {
        p1.add(new VarBlock("point", null, 1, VarTypes.INT));
        r1.add(new VarBlock("x", null, 1, VarTypes.INT));
        r1.add(new VarBlock("y", null, 1, VarTypes.INT));
        p2.add(new VarBlock("s", null, 1, VarTypes.INT));
        cb1 = new CodeBlock("Code __Block 1^%", null, 1, null, p1, r1);
        cb2 = new CodeBlock("println", null, 2, null, p2, r2, "System.out.");
        cb3 = new CodeBlock("println", null, 3, null, p2, r2);
        cb4 = new CodeBlock("emptyMethod",null,4,null,null,null);
        cb1.setTaggedCode(tc1);
        cb3.setTaggedCode(tc2);
        cb4.setTaggedCode(new LinkedList<TaggedCodePiece>());
        
        @SuppressWarnings("unused")
        RefBlock rb[] = {new RefBlock(5,cb2), new RefBlock(6,cb3), new RefBlock(7,cb4)};

        AccessTypes[] cb3Access = {AccessTypes.FINAL, AccessTypes.STATIC}; 
        cb3.setAccess(cb3Access);
        cb3Access[0] = AccessTypes.PUBLIC; 
        cb3.setAccess(cb3Access);
      }
      catch(Exception e)
      {
        System.err.println(e.getMessage());
        return;
      }
      RefBlock rb1 = new RefBlock(10, cb1);
      RefBlock rb2 = new RefBlock(11, cb2);
      RefBlock rb3 = new RefBlock(12, cb3);
      RefBlock rb4 = new RefBlock(13, cb4);
      rb2.setOwner(cb1);
      rb3.setOwner(cb4);
      rb4.setOwner(cb2);
      
      rb3.setBlock(cb2);
      //LinkedList<RefBlock> testParams = new LinkedList<RefBlock>();
      //testParams.add(rb1);
      
      rb2.addParam(rb1);
      
      System.out.println(cb3);
      
      System.out.println(rb1);
      System.out.println(rb2);
      System.out.println(rb3);
      System.out.println(rb4);
      System.out.println(cb3.getRefs().get(0));
   }

}
